package mails;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;

import javax.mail.Folder;
import javax.mail.FolderClosedException;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Store;
import javax.mail.event.MessageCountAdapter;
import javax.mail.event.MessageCountEvent;

import com.sun.mail.imap.IMAPFolder;

/**
 * This is an experimental sample meant to demonstrate listening for mail events
 * from QualysGuard and triggering some local action.
 * <P/>
 * This is not a supported product and is provided with no warranty, explicit or
 * implied.  Use at your own risk.
 */
public class EMailAgent implements Runnable, WorkerStateListener {

	private Thread runthread;
	
	private String host;
	private String userid;
	private String pw;
	private String folderName;
	
	private File trigger_defs;
	private List<Trigger> triggers = new ArrayList<Trigger>();
	
	private ConcurrentLinkedQueue<Worker> workers = new ConcurrentLinkedQueue<Worker>();
	
	private static ConcurrentLinkedQueue<String> buffer = new ConcurrentLinkedQueue<String>();

	public static void main(String argv[]) throws IOException {

		if (argv.length < 6) {
		    System.out.println(
			"Usage: EMailAgent <host> <user> <password> <mbox> <freq> <trigger pattern csv file name> [int: max worker threads]");
		    System.exit(1);
		}

		int maxthreads = 10;
		if( argv.length==7 ) maxthreads = Integer.parseInt(argv[6]);
		
		EMailAgent agent = new EMailAgent(argv[0],argv[1],argv[2],argv[3],argv[4],argv[5],maxthreads);
		
	}

	public EMailAgent(String host, String user, String password, String folderName, String delay, String trigger_defs_file, int max_workers) throws IOException{

		System.out.println("\nEmail Notification Agent Runnning ...\n");

		this.trigger_defs = new File(trigger_defs_file);
		
		this.host = host;
		this.userid = user;
		this.pw = password;
		this.folderName = folderName;

		for(int i=0; i<max_workers; i++){
			Worker w = new Worker();
			w.add(this);
			workers.add(w);
		}
		
		this.runthread = new Thread(this);
		this.runflag.set(true);
		this.runthread.start();
	}

	private long last_load_ts;

/*
 * compares the file timestamp and reloads if dirty
 */
	private void loadTriggerDefinitions() {

		if( trigger_defs.lastModified() == last_load_ts ){
			return;
		}
		
		BufferedReader br = null;
		try{
			br = new BufferedReader(new FileReader(this.trigger_defs));
			
			HashSet<Trigger> triggers = new HashSet<Trigger>();
			String line = null;
			while( (line = br.readLine()) != null ){
				if( line.trim().length() > 0 ){
					String[] tokens  = line.split(",");
					if( tokens.length > 1 ){
						triggers.add(new ShellTrigger(tokens[0],tokens[1]));
					}
				}
			}
			this.triggers.clear();
			this.triggers.addAll(triggers);

			System.out.println("loaded ["+triggers.size()+"] triggers");
			
			this.last_load_ts = trigger_defs.lastModified();

		}catch(IOException ioe){
			System.out.println("A problem occurred reading the trigger definitions from:"+trigger_defs.getAbsolutePath()+"\nContinuing but triggers may be out of date");
		}finally{
			if( br!=null )
				try {
					br.close();
				} catch (IOException e) {
					System.err.println("failed to close handle to trigger definition file");
					e.printStackTrace();
				}
		}
	}

	public void addTrigger(String regexString, String commandString){
		this.triggers.add( new ShellTrigger(regexString,commandString));
	}
	
	public void add(Trigger trigger){
		this.triggers.add(trigger);
	}

	private long wait_interval = 30*1000; //30 seconds;

	public void setInterval(long interval){
		this.wait_interval = interval;
		synchronized(triggers){
			triggers.notifyAll();
		}
	}

	private boolean assignToNextFreeWorker(Trigger T){
		for( Worker w : workers ){
			if( w.acceptWork(T) ) return true;
		}
		return false;
	}
	
	private Trigger matchTrigger(String test){
	    for( Trigger T : triggers ){
	    	if( T.accept(test) ) return T;
	    }
	    return null;
	}
	
	private AtomicBoolean runflag = new AtomicBoolean(true);
	public void run(){
		while(runflag.get()){

			try {
			    Properties props = System.getProperties();
			    Session session = Session.getInstance(props, null);
			    Store store = session.getStore("imap");
			    
			    
			    System.out.println("DEBUg: attempting to establish imap session...");
			    store.connect(this.host, this.userid, this.pw);

		    	Folder folder = store.getFolder(this.folderName);
			    if (folder == null || !folder.exists()) {
					System.out.println("Invalid folder:"+folderName);
			    }else{
				    folder.open(Folder.READ_WRITE);
				    folder.addMessageCountListener(
			    		new MessageCountAdapter() {
							public void messagesAdded(MessageCountEvent ev) {
								loadTriggerDefinitions();
							    Message[] msgs = ev.getMessages();
							    System.out.println("\nGot " + msgs.length + " new messages");
							    //check the message against the patterns for this folder
							    for (int i = 0; i < msgs.length; i++) {
									try {
										String test = msgs[i].getSubject();
										boolean processed = false;

										Trigger T = matchTrigger(test);
										if( T!=null ){
											if( ! assignToNextFreeWorker(T) ){
										    	buffer.add(test);
										    	System.out.println("DEBUG: buffering:["+test+"]");
											}
										}else{
											System.out.println("No Trigger registered for:["+test+"]");
										}
									    
									} catch (MessagingException mex) {
									    mex.printStackTrace();
									}
							    }
							}
			    		}
			    	);
				    System.out.println("Connected!");
				    
				    boolean supportsIdle = false;
				    try {
						if (folder instanceof IMAPFolder) {
						    IMAPFolder f = (IMAPFolder)folder;
						    f.idle();
						    supportsIdle = true;
						}
				    } catch (FolderClosedException fex) {
				    	throw fex;
				    } catch (MessagingException mex) {
				    	supportsIdle = false;
				    }

				    for (;;) {
						if (supportsIdle && folder instanceof IMAPFolder) {
						    IMAPFolder f = (IMAPFolder)folder;
						    f.idle();
						    System.out.println("done IDLING");
						} else {
							synchronized(triggers){
								triggers.wait(wait_interval);
							}
							System.out.println("done WAITING");
						    folder.getMessageCount();
						}
				    }
			    }
		
			} catch (Exception ex) {
			    ex.printStackTrace();
			}
		}
    }

	public void stateChanged(Worker subject) {
		System.out.println("DEBUG: Received Worker state change...");
		if( subject.getWorkerState() == Worker.STATE.IDLE ){
			String next = buffer.poll();
			if(next==null) return;

			System.out.println("NEXT: processing previously buffered msg key:"+next);
			
			Trigger trigger = matchTrigger(next);
			boolean processed = false;
			if( trigger!=null ) processed = subject.acceptWork(trigger);
			if(!processed ) buffer.add(next);  //not accepted by worker, then reinsert on the queue
		}
	}

}

interface WorkerStateListener {
	public void stateChanged(Worker subject);
}

class Worker extends Thread{

	enum STATE { IDLE, BUSY };
	
	private STATE state = STATE.IDLE;
	
	private AtomicBoolean busy = new AtomicBoolean(false);
	private Trigger current_task;

	Worker(){
		super.setDaemon(true);
		super.start();
	}

	private Set<WorkerStateListener> listeners = new HashSet<WorkerStateListener>();
	
	void add(WorkerStateListener listener){
		listeners.add(listener);
	}
	
	void remove(WorkerStateListener listener){
		listeners.remove(listener);
	}
	
	private void setState(STATE newState){
		this.state = newState;
		for( WorkerStateListener l : listeners ){
			l.stateChanged(this);
		}
	}
	
	public STATE getWorkerState(){ return this.state; }
	
	public boolean acceptWork(Trigger trigger){
		if( busy.get() ) return false;
		synchronized(Worker.this){
			busy.set(true);
			this.current_task = trigger;
			this.notify();
			return true;
		}
	}

	public void run(){
		while(true){
			try{
				synchronized(Worker.this){
					if( current_task == null ){
						System.out.println("waiting...");
						this.wait();
					}
					System.out.println("working...");
					current_task.fire();
					current_task = null;
					busy.set(false);
					setState(STATE.IDLE);
				}
			} catch (InterruptedException e) {
				e.printStackTrace();
			}finally{
				
			}
		}
	}

}

/**
 * base for classes that will be called to evaluate email message titles
 * <p/>
 * While the IO is captured immediately, there is no guarantee the stream will be available before the process returns control,
 * or that either the input or output streams will be available either.  It would be safest to assume no IO is possible until
 * control has returned.
 * <p/>
 * Triggers are equal if their regular expression definitions are equal
 * @author Qualys Technical Support
 *
 */
abstract class Trigger {
	
	int key;
	private Pattern constraint;

	Trigger(String regex){
		this.constraint = Pattern.compile(regex);
		key = regex.hashCode();
	}
	
	public int hashCode(){ return key; }
	public boolean equals(Object RHS){
		if( Trigger.class.isAssignableFrom( RHS.getClass() ) ){
			return this.constraint.equals( ((Trigger)RHS).constraint );
		}
		return false;
	}
	
	public boolean accept(String test){
		if( constraint.matcher(test).matches() ){
			return true;
		}else{
			return false;
		}
	}

	public abstract void fire();
}

/**
 * generic encapsulation of an array of command tokens and a regular expression.
 * <P/>
 * this trigger waits for the exit status/code of the spawned process and will block until the process exits.
 * @author Qualys Technical Support
 *
 */
class ShellTrigger extends Trigger {
	
	String cmdstring;
	String[] cmdtokens;

	ShellTrigger(String regex, String cmdstring){
		super(regex);
		this.cmdstring = cmdstring;
		this.cmdtokens = cmdstring.split(" ");
		key = regex.hashCode();
	}

	public void fire(){
		try {
			System.out.println("SHELL TRIGGER FIRING:["+cmdtokens[0]+"]");
			
			Process prc = Runtime.getRuntime().exec(cmdtokens);

			byte[] buffer = new byte[8192];
			BufferedInputStream bis = new BufferedInputStream(prc.getInputStream());
			int r;
			while( (r = bis.read(buffer)) > -1 ){
				System.out.print( new String(buffer,0,r) );
			}

			int exitVal = prc.waitFor();

			System.out.println("SHELL TRIGGER EXITED:["+exitVal+"]");
			
		} catch (IOException e) {
			e.printStackTrace();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

}
